package com.qisimanxiang.dalaran.zuul.rule;

import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
import com.alibaba.csp.sentinel.context.Context;
import com.alibaba.csp.sentinel.node.ClusterNode;
import com.alibaba.csp.sentinel.node.DefaultNode;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 将timeWindow调整为毫秒级，随机最大区间
 *
 * @author wangmeng
 * @date created in 6:02 下午 2019/9/16
 * @modified by
 */
@Slf4j
public class CustomerDegradeRule extends DegradeRule {

    private static final int RT_MAX_EXCEED_N = 5;

    @SuppressWarnings("PMD.ThreadPoolCreationRule")
    private static ScheduledExecutorService pool = Executors.newScheduledThreadPool(
            Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("sentinel-degrade-reset-task", true));

    public CustomerDegradeRule() {
    }

    public CustomerDegradeRule(String resourceName) {
        setResource(resourceName);
    }

    /**
     * RT threshold or exception ratio threshold count.
     */
    private double count;

    /**
     * 最大时间窗口 毫秒
     */
    private int timeWindow;
    /**
     * 最小时间窗口 毫秒
     */
    private int minTimeWindow = 10;

    private Random random = new Random();

    /**
     * Degrade strategy (0: average RT, 1: exception ratio).
     */
    private int grade = RuleConstant.DEGRADE_GRADE_RT;

    private final AtomicBoolean cut = new AtomicBoolean(false);

    public int getMinTimeWindow() {
        return minTimeWindow;
    }

    public DegradeRule setMinTimeWindow(int minTimeWindow) {
        this.minTimeWindow = minTimeWindow;
        return this;
    }

    @Override
    public int getGrade() {
        return grade;
    }

    @Override
    public DegradeRule setGrade(int grade) {
        this.grade = grade;
        return this;
    }

    private AtomicLong passCount = new AtomicLong(0);

    @Override
    public double getCount() {
        return count;
    }

    @Override
    public DegradeRule setCount(double count) {
        this.count = count;
        return this;
    }

    private boolean isCut() {
        return cut.get();
    }

    private void setCut(boolean cut) {
        this.cut.set(cut);
    }

    @Override
    public AtomicLong getPassCount() {
        return passCount;
    }

    @Override
    public int getTimeWindow() {
        return timeWindow;
    }

    @Override
    public DegradeRule setTimeWindow(int timeWindow) {
        this.timeWindow = timeWindow;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof CustomerDegradeRule)) {
            return false;
        }
        if (!super.equals(o)) {
            return false;
        }

        CustomerDegradeRule that = (CustomerDegradeRule) o;

        if (count != that.count) {
            return false;
        }
        if (timeWindow != that.timeWindow) {
            return false;
        }
        if (grade != that.grade) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + new Double(count).hashCode();
        result = 31 * result + timeWindow;
        result = 31 * result + grade;
        return result;
    }

    @Override
    public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {
        if (cut.get()) {
            return false;
        }

        ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(this.getResource());
        if (clusterNode == null) {
            return true;
        }

        if (grade == RuleConstant.DEGRADE_GRADE_RT) {
            double rt = clusterNode.avgRt();
            if (rt < this.count) {
                passCount.set(0);
                return true;
            }

            // Sentinel will degrade the service only if count exceeds.
            if (passCount.incrementAndGet() < RT_MAX_EXCEED_N) {
                return true;
            }
        } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) {
            double exception = clusterNode.exceptionQps();
            double success = clusterNode.successQps();
            double total = clusterNode.totalQps();
            // if total qps less than RT_MAX_EXCEED_N, pass.
            if (total < RT_MAX_EXCEED_N) {
                return true;
            }

            double realSuccess = success - exception;
            if (realSuccess <= 0 && exception < RT_MAX_EXCEED_N) {
                return true;
            }

            if (exception / success < count) {
                return true;
            }
        } else if (grade == RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) {
            double exception = clusterNode.totalException();
            if (exception < count) {
                return true;
            }
        }

        if (cut.compareAndSet(false, true)) {
            ResetTask resetTask = new ResetTask(this);
            int time = random.nextInt(timeWindow - minTimeWindow) + minTimeWindow;
            log.info("degrade time ：{} ms", time);
            pool.schedule(resetTask, time, TimeUnit.MILLISECONDS);
        }

        return false;
    }

    @Override
    public String toString() {
        return "DegradeRule{" +
                "resource=" + getResource() +
                ", grade=" + grade +
                ", count=" + count +
                ", limitApp=" + getLimitApp() +
                ", timeWindow=" + timeWindow +
                "}";
    }

    private static final class ResetTask implements Runnable {

        private CustomerDegradeRule rule;

        ResetTask(CustomerDegradeRule rule) {
            this.rule = rule;
        }

        @Override
        public void run() {
            rule.getPassCount().set(0);
            rule.cut.set(false);
        }
    }
}
